<template>
  <div class="scroller" ref="scroller" @scroll="onScroll">
    <div class="wrapper" :style="{ height: scrollBarHeight + 'px' }">
      <div :style="{ 'transform': `translate3d(0, ${scrollTop}px, 0)` }">
        <virtual-item v-for="item in visibleList" :item="item" :index="item.index" :key="item.brandId"
          @update-height="updateItemHeight">
          <template v-slot:default="scope">
            <div :style="{ height: `${scope.data.height}px`, 'line-height': `${scope.data.height}px` }">
              <div>{{ scope.data.name }}---Color Ly</div>
            </div>
          </template>
        </virtual-item>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import VirtualItem from './item.vue'

export default defineComponent({
  name: 'virtual-view',
  components: {
    VirtualItem
  },
  data () {
    return {
      visibleCount: 10, // 可视窗口展示项
      lengths: 10000, // 模拟列表项
      estimatedItemHeight: 90, // 每一项假定的高度
      startIndex: 0, // 截取数组的 起始 索引
      endIndex: 10, // 截取数组的 结束 索引
      scrollTop: 0, // 距离顶部的偏移量
      scrollBarHeight: 0, // 虚拟列表高度
      bufferItemCount: 4, // 缓冲加载项
      dataList: [] as any[], // 数据列表
      itemHeightCache: [] as any[], // 每一项高度
      itemTopCache: [] as any[] // 每一项距顶部的实际高度
    }
  },
  computed: {
    visibleList () {
      const bufferItemCount = this.bufferItemCount as number
      const max = (this.lengths - 10) as number
      const lists = this.dataList as any[]
      const endIndex = this.endIndex as number
      // fix bug 针对滚动到底部时，进行补丁修复
      const startIndex = (this.startIndex > max ? max : this.startIndex) as number
      return lists.slice(startIndex, endIndex + bufferItemCount)
    }
  },
  mounted () {
    this.dataList = this.getDataList()
    this.generateEstimatedItemData()
  },
  methods: {
    generateEstimatedItemData () {
      const estimatedTotalHeight = this.dataList.reduce((pre, current, index) => {
        // 给每一项一个虚拟高度
        this.itemHeightCache[index] = this.estimatedItemHeight
        // 给每一项距顶部的虚拟高度
        this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight
        return pre + this.estimatedItemHeight
      }, 0)
      // 列表总高
      this.scrollBarHeight = estimatedTotalHeight
    },
    updateItemHeight ({ index, height }: { index: number, height: number }) {
      const len = this.itemHeightCache.length
      // dom元素加载后得到实际高度 重新赋值回去
      this.itemHeightCache[index] = height
      // 重新确定列表的实际总高度
      this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
        return pre + current
      }, 0)
      const newItemTopCache = [0]
      for (let i = 1; i < len; i++) {
        // 虚拟每项距顶部高度 + 实际每项高度
        newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1]
      }
      // 获得每一项距顶部的实际高度
      this.itemTopCache = newItemTopCache
      // fix bug 针对滚动到底部时，进行补丁修复
      if (index === len - 1) {
        console.log('logger index', index)
        this.$nextTick(() => {
          this.scrollTop = this.scrollBarHeight - 600
        })
      }
    },
    getDataList () {
      return new Array(this.lengths).fill(0).map((_, i) => ({
        brandId: i + 1,
        index: i,
        name: `第${i + 1}项`,
        height: Math.random() * 100 + 40
      }))
    },
    // 获取渲染项起始索引
    getStartIndex (scrollTop: number) {
      // 每一项距顶部的距离
      const arrs = this.itemTopCache
      let left = 0
      let index = -1
      let right = arrs.length - 1
      let mid = Math.floor((left + right) / 2)
      // 判断 有可循环项时进入
      while (right - left > 1) {
        /*
          二分法：拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
          arrs[mid] 为虚拟列高度的中间项
          不断while 循环，利用二分之一将数组分割，减小搜索范围
          直到最终定位到 目标index 值
        */
        // 目标数在左侧
        if (scrollTop < arrs[mid]) {
          right = mid
          mid = Math.floor((left + right) / 2)
        } else if (scrollTop > arrs[mid]) {
          // 目标数在右侧
          left = mid
          mid = Math.floor((left + right) / 2)
        } else {
          index = mid
          return index
        }
      }
      index = left
      return index
    },
    onScroll () {
      const el = this.$refs.scroller as HTMLElement
      const scrollTop = el.scrollTop
      const startIndex = this.getStartIndex(scrollTop)
      // 如果是奇数开始，就取其前一位偶数
      if (startIndex % 2 !== 0) {
        this.startIndex = startIndex - 1
      } else {
        this.startIndex = startIndex
      }
      this.endIndex = this.startIndex + this.visibleCount
      this.scrollTop = this.itemTopCache[this.startIndex] || 0
    }
  }
})
</script>

<style scoped lang="scss">
.scroller {
  height: 600px;
  overflow: auto;
}

.wrapper {
  margin: 0;
  padding: 0;
}
</style>
